Spring Security权限绕过深入分析

前言

1
2
本来是没想把这个洞单独写篇文章的,因为也看到其他师傅已经发表了相应的文章。主要的绕过原理也比较简单。即使用正则表达式匹配路径时.*不会匹配\r或\n,因此当url中包含换行符则springsecurity使用RegexRequestMatcher(".*",null)正则表达式去匹配就会匹配不到从而导致了权限绕过。
当我复现这个漏洞时却发现虽然绕过了springsecurity的拦截,但是却访问不到对应的Controller。为了解决这个问题才有了这篇文章。

问题

​ 借用killer师傅文章中的Demo漏洞环境如下:

SpringSecurity配置

1
2
3
4
5
6
7
8
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.requestMatchers(new
RegexRequestMatcher(".*",null)).authenticated()//配置拦截所有请求
.antMatchers("/login").permitAll();//配置/login不拦截
}

Controller配置

1
2
3
4
5
6
@ResponseBody
@RequestMapping("/test/*")
public String test(){
return "test";
}

​ 正常访问被SpringSecurity规则拦截,如下所示:

image-20220522000105480

​ 通过%0a绕过,这里和其他师傅们的分析有点不同,虽然绕过了springsecurity的限制但是并没有访问到Controller。

image-20220522000241788

​ 因为不止一个师傅用上面的demo代码做了复现,所以刚开始我以为是自己哪里配置错了,但是反复的检查发现配置没有问题,我也确实绕过了springsecurity的检测,只是没有访问到Controller而已。后来找killer师傅要了他的环境,在绕过以后确实是可以请求到的。

image-20220522000753058

分析

​ 对比了两套环境的差异,发现主要的区别在springmvc的版本上,而且其实根据结果我们也可以猜出来。在第一套环境中springmvc再匹配由/test/11%0a的路由没匹配到而第二套环境匹配到了。所以我们深入的分析下到底是什么原因导致了这种差异。

​ 通过调试对比发现在DispatcherServlet#doDispatch中获取的mappedHandler是不同的,所以问题出现在getHandler的处理过程中。

1
2
3
4
5
6
   protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
mappedHandler = this.getHandler(processedRequest);
...

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

​ 循环调用保存的HandlerMapping#getHandler

1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();

while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}

​ 默认保存了下面5个HandlerMapping,通过简单查阅文档可知注解中配置的路由由RequestMappingHandlerMapping处理。

image-20220522001555337

getHandler中通过getHandlerInternal获取handler构建HandlerExecutionChain并返回。

1
2
3
4
5
6
7
8
9
10
11
 public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//获取handler
Object handler = this.getHandlerInternal(request);
。。。
//构建HandlerExecutionChain
HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
。。。
//返回
return executionChain;
}
}

getHandlerInternal从request对象中获取请求的path并根据path找到handlerMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//从request对象中获取path
String lookupPath = this.initLookupPath(request);
this.mappingRegistry.acquireReadLock();

HandlerMethod var4;
try {
//根据path找到handlerMethod
HandlerMethod handlerMethod = this.lookupHandlerMethod(lookupPath, request);
var4 = handlerMethod != null ? handlerMethod.createWithResolvedBean() : null;
} finally {
this.mappingRegistry.releaseReadLock();
}

return var4;
}

lookupHandlerMethod首先直接根据路径获取获取不到才使用addMatchingMappings遍历所有的ReuqestMappingInfo对象并进行匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<AbstractHandlerMethodMapping<T>.Match> matches = new ArrayList();
//直接根据路径匹配的,比如配置requestmapping为/test,则/test最为key、ReuqestMappingInfo对象为value保存在map中。然后以lookupPath为key直接从map获取即可。
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
this.addMatchingMappings(directPathMatches, matches, request);
}

if (matches.isEmpty()) {
// 获取不到则遍历this.mappingRegistry保存的所有ReuqestMappingInfo对象。
this.addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
private void addMatchingMappings(Collection<T> mappings, List<AbstractHandlerMethodMapping<T>.Match> matches, HttpServletRequest request) {
Iterator var4 = mappings.iterator();

while(var4.hasNext()) {
T mapping = var4.next();
//逐个匹配
T match = this.getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new AbstractHandlerMethodMapping.Match(match, (AbstractHandlerMethodMapping.MappingRegistration)this.mappingRegistry.getRegistrations().get(mapping)));
}
}

}

​ 在getMatchingMapping中不同版本的SpringMVC代码不太一样:

springmvc 5.3.20:

1
2
3
4
5
6
7
8
9
10
11
 public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
...
PathPatternsRequestCondition pathPatterns = null;
if (this.pathPatternsCondition != null) {
pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
if (pathPatterns == null) {
return null;
}
...

}

springmvc 5.1.9:

1
2
3
4
5
 public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
...
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
...

​ 也就是不同版本的springmvc使用了不同的RequestCondition导致的。经过简单的查阅资料PathPatternsRequestCondition是spring高版本引入的使用PathPattern来进行URL匹配。而在早些版本PatternsRequestCondition使用AntPathMatcher来进行匹配。

AntPathMatcher

​ 主要是由AntPathMatcher#doMatch完成匹配,首先通过tokenizePattern对路由中的配置的url进行分割,再调用tokenizePath对传入的PathUrl分割,最后将分割后的结果分别通过matchStrings进行匹配。

image-20220522101201839

​ 当匹配后面的换行时,由于这里得到的正则也是.*所以也匹配不到换行符,因此找不到对应的Controller进行处理。

image-20220522101804434

PathPattern

PathPatternsRequestCondition#getMatchingCondition将请求的URL转换为PathContainer对象并调用getMatchingPatterns进行匹配,匹配成功则创建PathPatternsRequestCondition并返回。

image-20220522102854732

​ 遍历PathPattern并进行匹配

image-20220522102519565

​ 通过SeparatorPathElement#matches匹配,SeparatorPathElement是分离器元素,默认是/

image-20220522103440307

​ 继续往下调用LiteralPathElement#matches逐个字符匹配。

image-20220522104335069

​ 由于PathContainerPathPattern中这里都保存的是test所以可以通过检测。

image-20220522104501082

​ 后面还有元素所以继续matches

image-20220522104638277

​ 最后针对*通过WildcardPathElement进行匹配。

image-20220522104713945

​ 在WildcardPathElement中只要pathElements的元素个数和PathPattern中的元素个数一致都会返回true。而元素个数的是由/分割的,换行符对元素分割没有影响,因此可以正常匹配。

image-20220522105317199

总结

​ SpringSecurity的权限认证绕过只能在高版本的springmvc中使用,由于低版本使用AntPathMatcher也是通过正则.*来匹配的URL路径的,因此在绕过SpringSecurity权限认证的同时也会绕过springmvc路由的匹配,导致匹配失败。